﻿using Devil;
using LitJson;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;

namespace GameToolkit.ContentProvider
{
    public enum ELoadTableFlags
    {
        Default = 0,
        Merge = 1, // 合并加载
        Mod = 2, // 从 StreamingAssets 加载 Mod 覆盖 
        ModVariant = 4, // Mod 文件夹自动添加 variant alias
    }

    public interface ITableMap<K, V>
    {
        bool TryGetValue(K key, out V value);
        void Set(K key, V value);
    }

    public class LoadTableThread : System.IDisposable, ICommandHandler, ICommandScheduler
    {
        internal abstract class Loader : /*IIdentified,*/ IYieldCommand
        {
            //public int Id { get; private set; }
            public int order { get; private set; }
            internal Loader(/*int id,*/ int order)
            {
                //this.Id = id;
                this.order = order;
            }
            //internal abstract ParallelDispatcher.Handler StartLoad();
            //internal abstract void Abort();
            public abstract IEnumerator Execute();
            //internal abstract void LoadAsTable();
        }

        // 
        internal class TableLoader : Loader, IBreakCommandSignal
        {
            TableSet mTable;
            string mData;
            string mOverrideData;
            string mFile;
            internal bool mModVariant;
            internal string mModFile;
            bool mMerge;
            internal bool mLogError;
            IAssetCache<TextAsset> mAsset;
            public ITable Table { get { return mTable; } }

            internal TableLoader(string file, bool merge, TableSet table, int order) : base(order)
            {
                mFile = file;
                mMerge = merge;
                mTable = table;
                mTable.StartLoading();
            }

            public override IEnumerator Execute()
            {
                mData = null;
                mOverrideData = null;
                ParallelDispatcher.Handler loadOverride = default;
                var streaming = Regex.IsMatch(mFile, @"(.+/)?assets/streamingassets/.+", RegexOptions.IgnoreCase);
                if (streaming)
                {
                    mData = NativeFileSystem.ReadAllText(mFile);
                }
                else
                {
                    var ptr = AssetsUtil.GetAssetAsync<TextAsset>(mFile);
                    if (ptr.IsValidInstance())
                    {
                        ptr.Retain();
                        mAsset = ptr;
                        yield return ParallelUtils.Schedule(GetAsset, ptr, true, ptr.GetParallelHandler());
                        mAsset = null;
                        ptr.Release();
                    }
                    if (!string.IsNullOrEmpty(mModFile) /*&& NativeFileSystem.HasFile(mModFile)*/)
                    {
                        var file = mModFile;
                        var group = mModVariant ? AssetsUtil.UtilInstance.Variants.GetVariantGroup(EVariantGroupQuery.Group, "Lang") : null;
                        if (mModVariant && group != null && !group.IsVariantAsset(file))// AssetsUtil.UtilInstance.Keywords.GetVariantAssetGroup(file) == null)// !ABVariant.IsVariantAsset(file))
                        {
                            var i = file.LastIndexOf('/');
                            if (i == -1)
                                file = ParallelUtils.Concat(group.ActiveVariant, '/', file);
                            else
                                file = ParallelUtils.Concat(file.Substring(0, i + 1), group.ActiveVariant, file.Substring(i));
                        }
                        loadOverride = ParallelUtils.Schedule(GetOverrideData, file, false);
                    }
                }
                LoadAsTable();
                yield return loadOverride;
                if (!string.IsNullOrEmpty(mOverrideData))
                {
                    mData = mOverrideData;
                    mMerge = true;
                    LoadAsTable();
                }
                mTable.FinishLoading();
#if UNITY_EDITOR
                RTLog.LogFormat(LogCat.Table, "{0} Table<{1}> @ {2}", mMerge ? "Merge" : "Load", mTable.DataType.Name, mFile);
#endif
                mTable = null;
            }

            void GetOverrideData(string file)
            {
                if (NativeFileSystem.HasFile(file))
                {
                    RTLog.LogFormat(LogCat.Table, "Will override <{0}> from {1}", mTable.DataType.Name, file);
                    mOverrideData = NativeFileSystem.ReadAllText(file);
                }
            }

            void GetAsset(IAssetPtr ptr)
            {
                var asset = ptr.Asset as TextAsset;
                if (asset != null)
                    mData = asset.text;
                else if (mLogError)
                    RTLog.LogErrorFormat(LogCat.Table, "Load Table<{0}> @{1} error.", mTable.DataType.Name, mFile);
            }

            void LoadAsTable()
            {
                if (!string.IsNullOrEmpty(mData))
                {
                    try
                    {
                        if (mMerge)
                        {
                            var tab = mTable.NewTableSet();// TableSet<T>.LoadAsNew(mFile, mData);
                            TableSet.LoadTo(mFile, mData, tab);
                            TableSet.MergeTo(tab, mTable);
                            tab.Dispose();
                        }
                        else
                        {
                            TableSet.LoadTo(mFile, mData, mTable);
                        }
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogErrorFormat("Load {0} as Table<{1}> error.\n{2}", mFile, mTable.DataType.Name, e);
                    }
                }
            }

            public void OnBreak()
            {
                if (mAsset != null)
                {
                    mAsset.Release();
                    mAsset = null;
                }
                if (mTable != null)
                {
                    mTable.FinishLoading();
#if UNITY_EDITOR
                    RTLog.LogFormat(LogCat.Table, "Abort {0} Table<{1}>", mMerge ? "Merge" : "Load", mTable.DataType.Name);
#endif
                    mTable = null;
                }
            }

            public override string ToString()
            {
                return $"TableLoad<{mFile}>";
            }
        }

        // 自动匹配表结构和对象， 结果
        internal class AutoLoader : Loader, IBreakCommandSignal
        {
            string mFile;
            string mData;
            LoadTableThread mThread;
            //Dictionary<int, TableSet> mPreset;
            ITableMap<string, ITable> mResults;
            TableSet mTable;
            IAssetPtr mAsset;

            internal AutoLoader(string file, LoadTableThread thread, ITableMap<string, ITable> result, int order) : base(order)
            {
                mFile = file;
                mThread = thread;
                mResults = result;
            }

            void GetAsset(IAssetPtr ptr)
            {
                var asset = ptr.Asset as TextAsset;
                if (asset != null)
                    mData = asset.text;
                //else if (mLogError)
                //    RTLog.LogErrorFormat(LogCat.Table, "Load Table<{0}> @{1} error.", mTable.DataType.Name, AssetPath);
            }

            public void OnBreak()
            {
                if (mAsset != null)
                {
                    mAsset.Release();
                    mAsset = null;
                }
                if (mTable != null)
                {
                    mTable.FinishLoading();
                    mTable = null;
                }
            }


            public override IEnumerator Execute()
            {
                var streaming = Regex.IsMatch(mFile, @"(.+/)?assets/streamingassets/.+", RegexOptions.IgnoreCase);
                if (streaming)
                {
                    mData = NativeFileSystem.ReadAllText(mFile);
                }
                else
                {
                    var ptr = AssetsUtil.GetAssetAsync<TextAsset>(mFile);
                    if (ptr != null)
                    {
                        ptr.Retain();
                        mAsset = ptr;
                        yield return ParallelUtils.Schedule(GetAsset, ptr, true, ptr.GetParallelHandler());
                        mAsset = null;
                        ptr.Release();
                    }
                }
                if (string.IsNullOrEmpty(mData))
                    yield break;
                var json = JsonMapper.ToObject(mData);
                if (json == null || json.Count == 0)
                    yield break;
                ITable table;
                if (!mResults.TryGetValue(mFile, out table))
                {
                    TableSet preset = null;
                    var fname = NativeFileSystem.GetFileName(mFile, true);
                    foreach (var tb in mThread.mAutoTables.Values)
                    {
                        if (tb.DataType.Name.EqualsIgnoreCase(fname))
                        {
                            preset = tb;
                            break;
                        }
                    }
                    if (preset == null)
                    {
                        var first = json[0];
                        var serialization = TableBase.CalculateSerializationID(first.Keys);
                        foreach (var tb in mThread.mAutoTables.Values)
                        {
                            if (tb.GetSerializationID() == serialization)
                            {
                                preset = tb;
                                break;
                            }
                        }
                    }
                    if (preset == null)
                    {
                        preset = mThread.mDefaultAutoTable;
                    }
                    if (preset != null)
                    {
                        table = preset.NewTableSet();
                        mResults.Set(mFile, table);
                    }
                }
                var tab = table as TableSet;
                if (tab == null)
                {
                    RTLog.LogErrorFormat(LogCat.Table, "Can't find table for file \"{0}\"", mFile);
                    yield break;
                }
                tab.LoadDatas(mFile, json);
#if UNITY_EDITOR
                RTLog.LogFormat(LogCat.Table, "Auto Load Table<{0}> @ {1}", tab.DataType.Name, mFile);
#endif
            }

            public override string ToString()
            {
                return $"AutoTabLoad<{mFile}>";
            }
        }

        string mTableFolder;
        string mTableExtension;

        readonly object _lock = new object();
        MinHeap<Loader> mLoaders;// = new Queue<Loader>();
        public bool LogError { get; set; }
        public bool EnabledAbort { get; set; }
        string mName;
        ParallelDispatcher.Handler handler;
        string mModFolder;
        Dictionary<System.Type, TableSet> mAutoTables;
        TableSet mDefaultAutoTable;
        //LoadCmd mCmd;

        public LoadTableThread(string tableFolder = "Assets/Tables/", string tableExtension = ".txt", string name = "default table loader")
        {
            if (tableFolder.EndsWith("/"))
                mTableFolder = tableFolder;
            else
                mTableFolder = ParallelUtils.Concat(tableFolder, "/");
            if (tableExtension.StartsWith(".") || string.IsNullOrEmpty(tableExtension))
                mTableExtension = tableExtension;
            else
                mTableExtension = ParallelUtils.Concat(".", tableExtension);
            EnabledAbort = true;
            mName = name;
            mLoaders = new MinHeap<Loader>(32, (a, b) => a.order - b.order);
        }

        public void SetModFolder(string folder)
        {
            if (string.IsNullOrEmpty(folder))
                mModFolder = null;
            else if (folder.EndsWithIgnoreCase("/"))
                mModFolder = folder;
            else
                mModFolder = ParallelUtils.Concat(folder, '/');
        }

        public ParallelDispatcher.Handler GetHandler()
        {
            return handler;
        }

        public void AddAutoTable<T>(bool asDefault = false) where T : TableBase, new()
        {
            lock (_lock)
            {
                if (mAutoTables == null)
                    mAutoTables = new Dictionary<System.Type, TableSet>();
                var type = typeof(T);
                TableSet tab;
                if (!mAutoTables.TryGetValue(type, out tab))
                {
                    tab = new TableSet<T>();
                    mAutoTables.Add(type, tab);
                }
                if (asDefault)
                    mDefaultAutoTable = tab;
            }
        }

        public void VisitAutoTables(System.Action<TableSet> autoTables)
        {
            lock (_lock)
            {
                if (mAutoTables != null && autoTables != null)
                {
                    foreach (var v in mAutoTables.Values)
                    {
                        autoTables(v);
                    }
                }
            }
        }

        public string GetFilePath(string file)
        {
            string path;
            if (file.StartsWithIgnoreCase(mTableFolder))
                path = file;
            else if (file.EndsWithIgnoreCase(mTableExtension))
                path = NativeFileSystem.Combine(mTableFolder, file);
            else
                path = NativeFileSystem.Combine(mTableFolder, ParallelUtils.Concat(file, mTableExtension));
            return path.ToLower();
        }

        public void LoadAutoTable(string file, ITableMap<string, ITable> results, int loadOrder = 0)
        {
            if (results == null || string.IsNullOrEmpty(file))
                return;
            lock (_lock)
            {
                if (mAutoTables == null)
                    return;
                string path = GetFilePath(file);
                var loader = new AutoLoader(path, this, results, loadOrder);
                //mIsLoading = true;
                mLoaders.Add(loader);
            }
        }

        public bool LoadTable<T>(string file, ELoadTableFlags type = 0, bool ignoreIfLoaded = true, int loadOrder = 0) where T : TableBase, new()
        {
            return LoadTable(file, TableSet<T>.Instance, type, ignoreIfLoaded, loadOrder);
        }

        public bool LoadTable(string file, TableSet table, ELoadTableFlags type = 0, bool ignoreIfLoaded = true, int loadOrder = 0)
        {
            if (string.IsNullOrEmpty(file))
                return false;
            lock (_lock)
            {
                string path, mod;
                if (file.StartsWithIgnoreCase(mTableFolder))
                {
                    path = file;
                    mod = file.Substring(mTableFolder.Length);
                }
                else if(file.StartsWithIgnoreCase("assets/"))
                {
                    path = file;
                    mod = null;
                }
                else
                {
                    mod = file;
                    if (!mod.EndsWithIgnoreCase(mTableExtension))
                        mod = ParallelUtils.Concat(file, mTableExtension);
                    path = NativeFileSystem.Combine(mTableFolder, mod);
                }
                if (ignoreIfLoaded && !table.JoinTable(path))
                {
#if UNITY_EDITOR
                    RTLog.LogFormat(LogCat.Table, "Skip load Table<{0}> @{1}", table.DataType.Name, path);
#endif
                    return false;
                }

                //int id = TableSet<T>.HashTable(file);
                var loader = new TableLoader(path, (type & ELoadTableFlags.Merge) != 0, table, loadOrder);
                if ((type & ELoadTableFlags.Mod) != 0 && !string.IsNullOrEmpty(mModFolder) && !string.IsNullOrEmpty(mod))
                {
                    loader.mModVariant = (type & ELoadTableFlags.ModVariant) != 0;
                    loader.mModFile = NativeFileSystem.Combine(mModFolder, mod);
                }
                loader.mLogError = LogError;
                //mIsLoading = true;
                mLoaders.Add(loader);
                return true;
            }
        }

        public void AbortAsyncTask()
        {
            lock (_lock)
            {
                if (EnabledAbort)
                {
                    mLoaders.Clear();
                    handler.Abort();
                    handler = default;
                    //mIsLoading = false;
                }
            }
        }

        public void Dispose()
        {
            lock (_lock)
            {
                mLoaders.Clear();
                handler.Abort();
                handler = default;
                //mIsLoading = false;
            }
        }

        public ParallelDispatcher.Handler Schedule(ParallelDispatcher.Handler dependency)
        {
            lock (_lock)
            {
                if (!handler.IsAlive && mLoaders.Count > 0)
                {
                    var cmd = new LoadCmd(this);
                    handler = ParallelUtils.Schedule(cmd, false, dependency);
                    //#if UNITY_EDITOR
                    //                    handler.SampleExecuteTime();
                    //#endif
                }
            }
            return handler;
        }

        class LoadCmd : IYieldCommand, IBreakCommandSignal, IExecutableProgress
        {
            LoadTableThread thread;

            //Loader currentLoader;
            ParallelDispatcher.CombinedHandler currentAction;
            public float Progress { get; private set; }

            public LoadCmd(LoadTableThread thread)
            {
                this.thread = thread;
            }
#if UNITY_EDITOR
            long startTime;
#endif

            public void OnBreak()
            {
#if UNITY_EDITOR
                var time = System.DateTime.Now.Ticks - startTime;
                RTLog.LogFormat(LogCat.Table, "Abort Thread[{0}] (process time: {1:0.##} ms)", thread.mName, time * 0.0001d);
#endif
                currentAction.AbortAll();
            }

            IEnumerator IYieldCommand.Execute()
            {
#if UNITY_EDITOR
                startTime = System.DateTime.Now.Ticks;
                RTLog.LogFormat(LogCat.Table, "Begin Thread[{0}]", thread.mName);
#endif
                var f = 1f / Mathf.Max(4, thread.mLoaders.Count);
                Progress = 0;
                var loop = true;
                //currentAction = ParallelUtils.NewCombination("tables");
                int order = int.MinValue;

                while (loop)
                {
                    currentAction = ParallelUtils.NewCombination("tables.queue");
                    // schedule loaders
                    while (loop)
                    {
                        Loader loader = null;
                        lock (thread._lock)
                        {
                            loop = thread.mLoaders.Count > 0;
                            if (loop)
                                loader = thread.mLoaders.RemoveTop();
                        }
                        if (loader != null)
                        {
                            if (loader.order != order)
                            {
                                order = loader.order;
                                yield return currentAction;
                                //current = SubThreading.NewCombination("tables.prev");
                            }
                            //currentLoader = loader;
                            var job = ParallelUtils.Schedule(loader, false);// loader.StartLoad();
                            currentAction.Combine(job);
                            //yield return currentAction;
                        }

                        Progress = Mathf.Clamp01(Progress + f);
                    }
                    yield return currentAction;
                    lock (thread._lock)
                    {
                        loop = thread.mLoaders.Count > 0;
                        if (!loop)
                            thread.handler = default;
                    }
                }
#if UNITY_EDITOR
                var time = System.DateTime.Now.Ticks - startTime;
                RTLog.LogFormat(LogCat.Table, "End Thread[{0}] (process time: {1:0.##} ms)", thread.mName, time * 0.0001d);
#endif
            }

            public override string ToString()
            {
                return thread.mName;
            }
        }
    }
}